大家在測試Application的時候一定都有這個經驗,如果我們今天是選擇直接連Testing Server的話,如果有非同步的task測試很容易就失敗了,其它實際例子還有Image Processing,Database Initialization等等,用[Day 20]範例來說Espresso測試程序已經跑到onView(withId(id)).check(matches(some condition))的時候結果非同步的callback還沒回來Espresso就會回傳無法matches指定情況的錯誤訊息,這時候通常大家都會用大絕招。Thread.sleep(millisecs)的方式來強迫Intrumentation Test Thread停下來等UI Thread,直到UI Thread做完callback再執行測試程式判斷的程式碼。這個做法可以暫時解決問題但是並不好,設定一個常數時間會有兩種問題
例如我們一樣用[Day 20]裡的的範例繼續延伸,一樣測試MainActivity裡的TextView,這裡我們預期會收到false的字串(這裡不用MockWebServer的假response,直接測試server連線部份)。測試程式因為不確定Server連線的callback何時會好所以sleep 10秒鐘來等待。
@Test
fun testAsynchronous() {
Thread.sleep(100000)
onView(withId(R.id.textView)).check(matches(withText("false")))
}
當然我們要選擇聰明一點的方法不用選擇上述的笨方法,Espresso有提供一個非常方便的類別叫做IdlingResource,實作的細節類似鎖的概念,在執行時把Intrumentation Test Thread鎖住,到指定的時機點再把它釋放,其中有針對不同狀況提供不同實作的IdlingResource,但我們這裡舉例最常用的CountingIdlingResource,先加入build.gradle的dependency。
implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'
我們先用一個object的類別來實作一個CountingIdlingResource的實體做為全域變數,我們一個測試期間只用一個IdlingResource,在Java中可用static class取代。其中傳入的參數是用來trace log用,可以選擇任意字串。
object Idling {
val idlingResource = CountingIdlingResource("test")
}
CountingIdlingResource是利用計數器的方式來做lock跟unlock的動作,計數器大於1的時候,Instrumentation Test Thread被鎖住,計數器等於0的時候Test Thread被釋放執行。
IdlingResource要被實作在production code裡,因為我們要鎖的是production code。
然後在Test時才會跟Instrumentation Test Thread註冊這個idlingResource,所以不會影響正常production code執行。
因此在ServerHelper的類別裡,我們在發出http reqeust前把Thread鎖住,等到callback回來的時候再把Thread釋放。
class ServerHelper() {
val sampleUrl = "http://dummy.restapiexample.com/api/v1/employee/1"
fun request(callback: (response: String) -> Unit) {
//開始http request前計數器+1鎖住testing thread
Idling.idlingResource.increment()
HandlerThread("demo").apply {
start()
Handler(looper).post {
val client = OkHttpClient()
val request = Request.Builder()
.url(sampleUrl)
.build()
try {
val response = client.newCall(request).execute()
val responseString = response.body?.string()
responseString?.let {
Handler(Looper.getMainLooper()).post {
callback(it)
//callback執行完成,計數器-1變成0後釋放testing thread
Idling.idlingResource.decrement()
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
}
在我們Production code設置好後我們來實作Testing code,我們命名一個testAsynchronous的function,在測試非同步前我們要先透過Espresso的IdlingRegistry註冊我們剛剛在prodcution code寫的IdlingResource,記得測試完後要release這個註冊實體。
當我們launch這個測試的時候,Espresso就會暫時停止測試等到callback回來才繼續下一步判斷我們matches裡的Assertion。
@Test
fun testAsynchronous() {
//註冊IdlingResource
IdlingRegistry.getInstance().register(Idling.idlingResource)
onView(withId(R.id.textView)).check(matches(withText("false")))
//釋放IdlingResource
IdlingRegistry.getInstance().unregister(Idling.idlingResource)
}